from typing import Tuple, Optional, List, Any, Dict
import time
from decimal import Decimal
from datetime import datetime
import urllib.parse
import requests
import json

from sqlalchemy.orm.session import Session
from selenium.webdriver.common.by import By
from selenium.webdriver import Chrome
from selenium.webdriver.remote.webelement import WebElement
from selenium.webdriver.common.action_chains import ActionChains
from selenium.common.exceptions import StaleElementReferenceException
from selenium.common.exceptions import NoSuchElementException
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import TimeoutException

from .browser import Browser
from .utils import ShippingData
from utils import wait, show_runtime
from db.utils import (
    get_engine_and_session_of_db,
    close_engine_and_session_of_db,
    filter_new_shipments,
    add_list_of_shipments
)
import settings


class SeleniumScraper:
    """ Инструменты для парсинга связанные с Selenium """

    def scroll_the_page(self, browser, a=.2, b=.5, w=0, h=50):
        """
        Прокрутить страницу js
        w - прокрутить страницу по ширине
        h - прокрутить страницу по высоте
        """

        browser.execute_script(f"window.scrollBy({w}, {h})")
        wait(a, b)


    def scroll_page_with_delay(self, browser: Chrome) -> None:
        """ Пролистываем всю страницу с задержкой """

        scroll_to_bottom_script: str = """
        let finish = undefined;
        const boo = setInterval(foo, 200);

        function foo() {
          if (window.scrollY < document.body.offsetHeight - 1000) {
            window.scrollBy(0, 100);
          } else {
            finish = true;
            clearInterval(boo);
          };
        };
        """
        browser.execute_script(scroll_to_bottom_script)

        finished_script: str = 'return window.scrollY > document.body.offsetHeight - 1000;'

        while True:
            if browser.execute_script(finished_script): break
            wait(.9, 1.1)

    def move_and_click_on_element(
        self,
        browser: Chrome,
        xpath: Optional[str] = None,
        element: Optional[WebElement] = None
    ) -> None:
        """ Перемещаемся к элементу и кликаем по нему """

        if xpath is None and element is None:
            message: str = 'Не было передано xpath и element'
            raise ValueError(message)

        while True:
            if xpath:
                element = self.get_page_element_wait(browser, xpath)

            try:
                actions = ActionChains(browser)
                actions.move_to_element(element)
                actions.perform()
                self.scroll_the_page(browser, h=500)
                wait(.3, .5)
                element.click()
                wait(.2, .4)
                break
            except StaleElementReferenceException:
                pass

    def move_and_click_on_elements(self, browser: Chrome, xpath: str) -> None:
        """ Перемещаемся к элементам и кликаем по ним """

        list_of_elements: List[WebElement] = browser.find_elements(
            By.XPATH,
            xpath
        )
        for element in list_of_elements:
            self.move_and_click_on_element(browser, element=element)

    def get_page_element_wait(
        self,
        browser: Chrome,
        xpath: str,
        show_message: Optional[bool] = None
    ) -> WebElement:
        """ Получаем элемент старницы ожидая его появления """

        while True:
            try:
                element = browser.find_element(By.XPATH, xpath)
                return element
            except NoSuchElementException as error:
                message = f'Элемент по {xpath} не был найден, ждем...'
                if show_message: print(message)
                wait(.5, .6)


    def get_elements_from_page(self, browser: Chrome, xpath: str) -> List[WebElement]:
        """ Получаем список элементов со страницы """

        list_of_elements: List[WebElement] = browser.find_elements(
            By.XPATH,
            xpath
        )
        return list_of_elements


    def wait_for_element(
        self,
        browser: Chrome,
        xpath: str,
        waiting_time: int,
        show_message: Optional[bool] = None
    ) -> Optional[WebElement]:
        """ Ждем появление элемента на странице """

        count: int = 0

        while True:
            if count > waiting_time: break
            try:
                element: WebElement = browser.find_element(By.XPATH, xpath)
                return element
            except NoSuchElementException as error:
                message = f'Ожидаем появление элемента {xpath}...'
                if show_message: print(message)
                wait(.9, 1.1)
                count += 1

    def enter_text_in_element(
        self,
        browser: Chrome,
        xpath: str,
        text: str
    ) -> None:
        """ Вводим текст в элемент """

        element: WebElement = self.get_page_element_wait(browser, xpath)

        for symbol in text:
            element.send_keys(symbol)
            wait(.2, .3)


    def click_on_element(self, browser: Chrome, xpath: str) -> None:
        """ Кликаем по элементу """

        element: WebElement = self.get_page_element_wait(browser, xpath)
        element.click()
        wait(.4, .5)


    def open_page(
        self,
        link: str,
        browser: Chrome,
        xpath: str,
        waiting_time: int
    ) -> None:
        """ Открываем какую-то страницу дожидаясь появления какого-то элемента """

        browser.get(link)

        while True:
            element: Optional[WebElement] = \
                self.wait_for_element(browser, xpath, waiting_time)
            if element: break
            browser.refresh()


class Scraper(Browser, SeleniumScraper):
    """ Общие инструменты для парсинга """
    pass


class AuthorizationScraper(Scraper):
    """ Инструменты для парсинга связанные с авторизацией на сайте """

    def __init__(self):
        self.AUTHORIZATION_LINK = 'https://www.centraldispatch.com/protected'

    def enter_login(self, browser: Chrome, login: str) -> None:
        """ Вводим логин пользователя """
        self.enter_text_in_element(
            browser,
            '//input[@id="Username"]',
            login
        )

    def enter_password(self, browser: Chrome, password: str) -> None:
        """ Вводим пароль пользователя """
        self.enter_text_in_element(
            browser,
            '//input[@id="password"]',
            password
        )

    def enter_required_data(
        self,
        browser: Chrome,
        account: Tuple[str, str]
    ) -> None:
        """ Вводим необходимые данные для авторизации """

        print('Вводим логин')
        self.enter_login(browser, account.login)
        print('Вводим пароль')
        self.enter_password(browser, account.password)

    def click_on_authorization_button(self, browser: Chrome) -> None:
        """ Нажимаем на кнопку авторизации """
        self.click_on_element(
            browser,
            '//button[@id="loginButton"]'
        )


    def check_open_page(self, browser: Chrome) -> Optional[WebElement]:
        """ Проверяем открывшуюся страницу, узнаем прошла ли авторизация """

        result: Optional[WebElement] = self.wait_for_element(
            browser,
            '//h1[contains(text(), "Account Dashboard")]',
            5
        )
        return result

    @show_runtime('Авторизация пользователя')
    def perform_authorization(
        self,
        browser: Chrome,
        account: Tuple[str, str]
    ) -> None:
        """ Выполняем авторизацию на сайте """

        while True:
            print('Открываем страницу')
            self.open_page(
                self.AUTHORIZATION_LINK,
                browser,
                '//input[@id="Username"]',
                7
            )
            self.enter_required_data(browser, account)
            print('Клик по кнопке')
            self.click_on_authorization_button(browser)

            if self.check_open_page(browser):
                print('Aвторизация выполнена')
                break
            print('Авторизация не прошла')


class ShipmentsScraperRequests(Scraper):
    """ Инструменты связанные с парсингом перевозок с помощью библиотеки requests """

    def open_shipping_page(self, browser: Chrome, link_for_parsing: str) -> None:
        """ Открываем страницу с перевозками """

        self.open_page(
            link_for_parsing,
            browser,
            '//*[@title="Add to Worklist"]',
            35
        )

        while True:
            if browser.current_url == link_for_parsing: break
            self.open_page(
                link_for_parsing,
                browser,
                '//*[@title="Add to Worklist"]',
                35
            )
        time.sleep(5)


    def get_token_for_request(self, list_of_logs: List[Dict]) -> str:
        """ Получаем токен для запроса """

        necessary_link: str = 'https://prod-search-app-bff.awscal2.manheim.com/api/open-search'

        for element_str in list_of_logs:
            if not necessary_link in str(element_str): continue

            element = json.loads(element_str['message'])

            try:
                element['message']['params']['request']['headers']['Authorization']
            except KeyError:
                continue

            if element['message']['params']['request']['url'] == necessary_link:
                token: str = element['message']['params']['request']['headers']['Authorization']
                return token


    def get_filters_for_request(
        self,
        list_of_logs: List[Dict]
    ) -> Dict:
        """ Получаем фильтры для запроса """

        list_of_filters: List[Dict] = []
        necessary_link: str = 'https://prod-search-app-bff.awscal2.manheim.com/api/open-search'

        for element_str in list_of_logs:
            if not necessary_link in str(element_str): continue

            element: Dict = json.loads(element_str['message'])

            try:
                element['message']['params']['request']['postData']
            except KeyError:
                continue

            if element['message']['params']['request']['url'] == necessary_link:
                filters_str: str = \
                    element['message']['params']['request']['postData']
                filters: Dict = json.loads(filters_str)
                list_of_filters.append(filters)

        return list_of_filters[-1]


    def get_shipping_list(self, token: str, filters_for_request: Dict) -> Dict:
        """ Получаем список перевозок """

        link: str = 'https://prod-search-app-bff.awscal2.manheim.com/api/open-search'

        headers: Dict[str, str] = {
            'accept': 'application/json',
            'authorization': token,
            'content-type': 'application/json'
        }
        response = requests.post(
            link,
            headers=headers,
            data=json.dumps(filters_for_request)
        )
        shipping_list: Dict = response.json()
        return shipping_list


    def convert_shipping_list(self, shipping_list: Dict) -> List[ShippingData]:
        """ Преобразуем список перевозок в нужный формат """

        def get_order_id() -> str:
            """ Получаем id перевозки """

            nonlocal shipping
            return str(shipping['id'])

        def get_price() -> Decimal:
            """ Получаем цену """

            nonlocal shipping
            price: Decimal = round(Decimal(shipping['price']['total']), 2)
            return price

        def get_number_of_miles() -> int:
            """ Получаем количество миль """

            nonlocal shipping
            return shipping['distance']

        def add_car_information(dictionary) -> None:

            nonlocal car_information

            for key in dictionary:
                if type(dictionary[key]) == dict:
                    add_car_information(dictionary[key])
                else:
                    if not dictionary[key] or key == 'id': continue
                    car_information += f'{key}: {dictionary[key]}\n'

        def get_car_information() -> None:
            """ Получаем информацию о машине """

            nonlocal shipping

            for dictionary in shipping['vehicles']:
                add_car_information(dictionary) 

        def get_pick_up_location() -> str:

            nonlocal shipping
            state: str = shipping['origin']['state']
            city: str = shipping['origin']['city']
            zip: str = shipping['origin']['zip']

            pick_up_location: str = f'{state}: {city}, {zip}'
            return pick_up_location

        def get_delivery_location() -> str:

            nonlocal shipping
            state: str = shipping['destination']['state']
            city: str = shipping['destination']['city']
            zip: str = shipping['destination']['zip']
            pick_up_location: str = f'{state}: {city}, {zip}'
            longitude: float = shipping['destination']['geoCode']['longitude']
            latitude: float = shipping['destination']['geoCode']['latitude']
            link: str = f'https://www.google.com/maps?q={latitude},{longitude}&ll={latitude},{longitude}&z=10'
            pick_up_location: str = f'{pick_up_location}\n\n\n{link}'
            return pick_up_location

        def get_pick_up_or_after_date() -> datetime:

            nonlocal shipping
            pick_up_or_after_date: str = shipping['availableDate']
            pick_up_or_after_date: str = pick_up_or_after_date.split('T')[0]
            pick_up_or_after_date: datetime = datetime.strptime(
                pick_up_or_after_date,
                '%Y-%m-%d'
            )
            return pick_up_or_after_date

        def get_company_name() -> str:
            """ Получаем название компании """

            nonlocal shipping
            company_name: str = shipping['shipper']['companyName']
            return company_name

        def get_phone_number() -> str:
            """ Получаем телефон """

            nonlocal shipping
            phone: str = shipping['shipper']['phone']
            return phone

        def get_hours() -> str:
            """ Получаем часы работы """

            nonlocal shipping
            hours: str = shipping['shipper']['hoursOfOperation']
            return hours

        def get_rating_in_old_system_percent() -> float:
            """ Получаем рейтинг по старой системе в проценте """

            nonlocal shipping
            rating_in_old_system_percent: float = shipping['shipper']['rating']
            return rating_in_old_system_percent

        def get_rating_in_old_system_overall() -> int:
            """ Получаем общий рейтинг по старой системе """

            nonlocal shipping
            rating_in_old_system_overall: int = shipping['shipper']['numberOfRatings']
            return rating_in_old_system_overall

        def get_rating() -> float:
            """ Получаем рейтинг """

            nonlocal shipping
            rating: int = shipping['shipper']['overallRating']['averageRating']
            return rating

        def get_overall_rating() -> int:
            """ Получаем общий рейтинг """

            nonlocal shipping
            overall_rating: int = shipping['shipper']['overallRating']['totalAmount']
            return overall_rating

        def get_additional_info() -> str:
            """ Получаем дополнительную информацию """

            nonlocal shipping
            additional_info: int = shipping['additionalInfo']
            if additional_info:
                return additional_info
            else:
                return 'Unspecified'

        def get_price_per_mile() -> float:
            """ Получаем цену за одну милю """

            nonlocal price
            nonlocal number_of_miles

            if not number_of_miles or number_of_miles < 0: return 0.0
            price_per_mile: float = price / number_of_miles
            price_per_mile: Decimal = round(price_per_mile, 2)
            return price_per_mile

        def get_posted_date() -> datetime:

            nonlocal shipping
            created_date: str = shipping['createdDate']
            created_date: str = created_date.split('T')[0]
            created_date: datetime = datetime.strptime(
                created_date,
                '%Y-%m-%d'
            )
            return created_date


        # ВЫШЕ ОПРЕДЕЛЕНИЕ ФУНКЦИЙ

        list_of_shipments: List[ShippingData] = []
        shipping_list: List[Dict] = shipping_list['items']

        for shipping in shipping_list:

            order_id: str = get_order_id()
            price: Decimal = get_price()
            number_of_miles: int = get_number_of_miles()
            price_per_mile = get_price_per_mile()
            car_information: str = ''
            get_car_information()
            pick_up_location: str = get_pick_up_location()
            delivery_location: str = get_delivery_location()
            pick_up_or_after_date: datetime = get_pick_up_or_after_date()
            posted_date: datetime = get_posted_date()
            company_name: str = get_company_name()
            phone_number: str = get_phone_number()
            hours: str = get_hours()
            rating_in_old_system_percent: float = get_rating_in_old_system_percent()
            rating_in_old_system_overall: int = get_rating_in_old_system_overall()
            rating: float = get_rating()
            overall_rating: int = get_overall_rating()
            additional_info: str = get_additional_info()
            desired_delivery_date = datetime(year=1999, month=1, day=1)

            shipping_data: ShippingData = ShippingData(
                order_id=order_id,
                price=price,
                number_of_miles=number_of_miles,
                price_per_mile=price_per_mile,
                car_information=car_information,
                pick_up_location=pick_up_location,
                delivery_location=delivery_location,
                pick_up_or_after_date=pick_up_or_after_date,
                desired_delivery_date=desired_delivery_date,
                posted_date=posted_date,
                company_name=company_name,
                phone_number=phone_number,
                hours=hours,
                rating_in_old_system_percent=rating_in_old_system_percent,
                rating_in_old_system_overall=rating_in_old_system_overall,
                rating=rating,
                overall_rating=overall_rating,
                additional_info=additional_info
            )
            list_of_shipments.append(shipping_data)

        return list_of_shipments


    @show_runtime('Получение новых перевозок')
    def get_new_shipments_from_page(
        self,
        session: Session,
        browser: Chrome,
        link_for_parsing: str
    ) -> List[ShippingData]:
        """ Получаем новые перевозки со страницы """

        print('Открываем страницу с перевозками')
        self.open_shipping_page(browser, link_for_parsing)
        list_of_logs: List[Dict] = browser.get_log('performance')

        print('Получаем токен')
        token: str = self.get_token_for_request(list_of_logs)
        print('Получаем фильтры')
        filters_for_request: Dict = self.get_filters_for_request(
            list_of_logs
        )
        print('Получаем список перевозок')
        shipping_list: Dict = self.get_shipping_list(token, filters_for_request)
        print('Преобразуем перевозки в нужный вид')
        converted_shipping_list: List[ShippingData] = \
            self.convert_shipping_list(shipping_list)
        print('Получаем новые перевозки')
        list_of_new_shipments: List[ShippingData] = \
            filter_new_shipments(session, converted_shipping_list)
        print('Новых перевозок: ', len(list_of_new_shipments))

        return list_of_new_shipments


class TrackShipmentsScraper(
    AuthorizationScraper,
    ShipmentsScraperRequests
):
    """ Инструменты для парсинга связанные с отслеживанием перевозок """

    @show_runtime('Уведомление о новых перевозках')
    def send_shipments_to_telegram(self, list_of_shipments: List[ShippingData]) -> None:
        """ Отправляем перевозки в телеграм """

        def get_message(shipping: ShippingData) -> str:
            """ Получаем сообщение для отправки в телеграм """

            delivery_location, delivery_location_link = \
                shipping['delivery_location'].split('\n\n\n')

            if shipping['desired_delivery_date'] == datetime(year=1999, day=1, month=1):
                desired_delivery_date = 'Unspecified'
            else:
                desired_delivery_date = \
                    shipping['desired_delivery_date'].strftime('%m/%d/%y')

            pick_up_or_after_date: str = \
                shipping['pick_up_or_after_date'].strftime('%m/%d/%y')

            posted_date: str = \
                shipping['posted_date'].strftime('%m/%d/%y')

            message: str = '1. ID - {}\n2. Цена, количество миль, цена за милю - ${}, {}mi, ${}/mi\n3. Информация о машине:\n{}\n4. Pick-Up Location - {}\n5. Delivery Location - {} {}\n6. Pick-Up on or After Date - {}\n7. Posted Date - {}\n8. Company - {}\n9. Phone/hours - {}/{}\n10. Rating in Old System - {}% ({})\n11. Overall Rating - {}/5 ({})\n12. Additional Info - {}' \
                .format(
                    shipping['order_id'],
                    shipping['price'],
                    shipping['number_of_miles'],
                    shipping['price_per_mile'],
                    shipping['car_information'],
                    shipping['pick_up_location'],
                    delivery_location,
                    delivery_location_link,
                    pick_up_or_after_date,
                    posted_date,
                    shipping['company_name'],
                    shipping['phone_number'],
                    shipping['hours'],
                    shipping['rating_in_old_system_percent'],
                    shipping['rating_in_old_system_overall'],
                    shipping['rating'],
                    shipping['overall_rating'],
                    shipping['additional_info']
                )
            encoded_message = urllib.parse.quote(message.encode('utf-8'))
            return encoded_message


        # ВЫШЕ ОПРЕДЕЛЕНИЕ ФУНКЦИЙ

        part_of_request: str = 'https://api.telegram.org/bot{}/sendMessage?chat_id={}' \
            .format(
                settings.TELEGRAM_BOT_TOKEN,
                settings.TELEGRAM_CHAT_ID
            )

        for shipping in list_of_shipments:
            message: str = get_message(shipping)
            request = f'{part_of_request}&text={message}'
            requests.get(request)


    def start(self, link_for_parsing: str, account: Tuple[str, str]) -> None:
        """ Начинаем отслеживать перевозки """

        engine, session = get_engine_and_session_of_db()
        browser: Chrome = self.get_browser()

        self.perform_authorization(browser, account)

        list_of_new_shipments: List[ShippingData] = \
            self.get_new_shipments_from_page(session, browser, link_for_parsing)

        add_list_of_shipments(session, list_of_new_shipments)
        self.send_shipments_to_telegram(list_of_new_shipments)

        self.close_browser(browser)
        close_engine_and_session_of_db(engine, session)
